
// Copyright (C) 2009-2012 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Diagnostics;
using Derm.OpenGL;

namespace Derm.Render
{
	/// <summary>
	/// Blend render state (buffer group, color).
	/// </summary>
	public sealed class BlendState : RenderState
	{
		#region Constructors
		
		/// <summary>
		/// Construct a default BlendState (blending disabled).
		/// </summary>
		public BlendState()
		{
			
		}

		/// <summary>
		/// Construct a BlendState with unified RGB/Alpha function.
		/// </summary>
		/// <param name="equation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending.
		/// </param>
		/// <param name="srcFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the source color (including alpha).
		/// </param>
		/// <param name="dstFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the destination color (including alpha).
		/// </param>
		public BlendState(BlendEquation equation, BlendFactor srcFactor, BlendFactor dstFactor)
			: this(equation, equation, srcFactor, srcFactor, dstFactor, dstFactor, new ColorRGBAF())
		{
			
		}

		/// <summary>
		/// Construct a BlendState with unified RGB/Alpha function.
		/// </summary>
		/// <param name="equation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending.
		/// </param>
		/// <param name="srcFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the source color (including alpha).
		/// </param>
		/// <param name="dstFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the destination color (including alpha).
		/// </param>
		/// <param name="constColor">
		/// A <see cref="ColorRGBAF"/> that specify the constant color used in blending functions.
		/// </param>
		public BlendState(BlendEquation equation, BlendFactor srcFactor, BlendFactor dstFactor, ColorRGBAF constColor)
			: this(equation, equation, srcFactor, srcFactor, dstFactor, dstFactor, constColor)
		{
			
		}

		/// <summary>
		/// Construct a BlendState with separated RGB/Alpha functions.
		/// </summary>
		/// <param name="rgbEquation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending RGB color components.
		/// </param>
		/// <param name="alphaEquation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending Alpha color component.
		/// </param>
		/// <param name="srcRgbFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the source color (alpha component excluded).
		/// </param>
		/// <param name="srcAlphaFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to only the source alpha component.
		/// </param>
		/// <param name="dstRgbFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the destination color (alpha component excluded).
		/// </param>
		/// <param name="dstAlphaFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to only the destination alpha component.
		/// </param>
		public BlendState(BlendEquation rgbEquation, BlendEquation alphaEquation, BlendFactor srcRgbFactor, BlendFactor srcAlphaFactor, BlendFactor dstRgbFactor, BlendFactor dstAlphaFactor)
			: this(rgbEquation, alphaEquation, srcRgbFactor, srcAlphaFactor, dstRgbFactor, dstAlphaFactor, new ColorRGBAF())
		{
			
		}

		/// <summary>
		/// Construct a BlendState with separated RGB/Alpha functions.
		/// </summary>
		/// <param name="rgbEquation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending RGB color components.
		/// </param>
		/// <param name="alphaEquation">
		/// A <see cref="BlendEquation"/> flag indicating which equation to used for blending Alpha color component.
		/// </param>
		/// <param name="srcRgbFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the source color (alpha component excluded).
		/// </param>
		/// <param name="srcAlphaFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to only the source alpha component.
		/// </param>
		/// <param name="dstRgbFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to the destination color (alpha component excluded).
		/// </param>
		/// <param name="dstAlphaFactor">
		/// A <see cref="BlendFactor"/> that specify the scaling factors applied to only the destination alpha component.
		/// </param>
		/// <param name="constColor">
		/// A <see cref="ColorRGBAF"/> that specify the constant color used in blending functions.
		/// </param>
		public BlendState(BlendEquation rgbEquation, BlendEquation alphaEquation, BlendFactor srcRgbFactor, BlendFactor srcAlphaFactor, BlendFactor dstRgbFactor, BlendFactor dstAlphaFactor, ColorRGBAF constColor)
		{
			if (IsSupportedEquation(rgbEquation) == false)
				throw new ArgumentException("not supported blending equation " + srcRgbFactor, "rgbEquation");
			if (IsSupportedEquation(alphaEquation) == false)
				throw new ArgumentException("not supported blending equation " + alphaEquation, "rgbEquation");
			if (IsSupportedFunction(srcRgbFactor) == false)
				throw new ArgumentException("not supported blending function " + srcRgbFactor, "srcRgbFactor");
			if (IsSupportedFunction(srcAlphaFactor) == false)
				throw new ArgumentException("not supported blending function " + srcAlphaFactor, "srcAlphaFactor");
			if (IsSupportedFunction(dstRgbFactor) == false)
				throw new ArgumentException("not supported blending function " + dstRgbFactor, "dstRgbFactor");
			if (IsSupportedFunction(dstAlphaFactor) == false)
				throw new ArgumentException("not supported blending function " + dstAlphaFactor, "dstAlphaFactor");

			// Blend enabled
			mEnabled = true;

			// Store RGB separate equation
			mRgbEquation = rgbEquation;
			// Store alpha separate equation
			mAlphaEquation = alphaEquation;

			// Store rgb separate function
			mRgbSrcFactor = srcRgbFactor;
			mRgbDstFactor = dstRgbFactor;
			// Store alpha separate function
			mAlphaSrcFactor = srcAlphaFactor;
			mAlphaDstFactor = dstAlphaFactor;
			// Store blend color
			mBlendColor = constColor;

			if ((EquationSeparated == true) && (RenderContext.CurrentCaps.BlendEquationSeparate.Supported == false))
				throw new InvalidOperationException("not supported separated blending equations");
			if ((FunctionSeparated == true) && (RenderContext.CurrentCaps.BlendFuncSeparate.Supported == false))
				throw new InvalidOperationException("not supported separated blending functions");
		}

		/// <summary>
		/// Construct the current DepthTestState.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> defining this RenderState.
		/// </param>
		public BlendState(RenderContext ctx)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (ctx.DebugEnabled) {
				if (ctx.IsCurrent() == false)
					throw new ArgumentException("not current", "ctx");
			}

			int blendRgbEquation, blendAlphaEquation;
			int blendRgbSrcFunct, blendAlphaSrcFunct;
			int blendRgbDstFunct, blendAlphaDstFunct;

			if (ctx.Caps.BlendEquationSeparate.Supported) {
				// Blend equation (RGB)
				Gl.Get(Gl.BLEND_EQUATION_RGB, out blendRgbEquation);
				RenderException.DebugCheckErrors();
				// Blend equation (Alpha)
				Gl.Get(Gl.BLEND_EQUATION_ALPHA, out blendAlphaEquation);
				RenderException.DebugCheckErrors();
			} else {
				if (ctx.Caps.BlendMinMax.Supported) {
					// Blend equation (RGBA)
					Gl.Get(Gl.BLEND_EQUATION, out blendRgbEquation);
					RenderException.DebugCheckErrors();
					// Alpha equation is the same for RGB!
					blendAlphaEquation = blendRgbEquation;
				} else {
					blendRgbEquation = blendAlphaEquation = (int)BlendEquation.Add;
				}
			}

			if (ctx.Caps.BlendFuncSeparate.Supported) {
				// Blend source function (RGB)
				Gl.Get(Gl.BLEND_SRC_RGB, out blendRgbSrcFunct);
				RenderException.DebugCheckErrors();
				// Blend source function (Alpha)
				Gl.Get(Gl.BLEND_SRC_ALPHA, out blendAlphaSrcFunct);
				RenderException.DebugCheckErrors();
				// Blend destination function (RGB)
				Gl.Get(Gl.BLEND_DST_RGB, out blendRgbDstFunct);
				RenderException.DebugCheckErrors();
				// Blend destination function (Alpha)
				Gl.Get(Gl.BLEND_DST_ALPHA, out blendAlphaDstFunct);
				RenderException.DebugCheckErrors();
			} else {
				// Blend source function (RGBA)
				Gl.Get(Gl.BLEND_SRC, out blendRgbSrcFunct);
				RenderException.DebugCheckErrors();
				blendAlphaSrcFunct = blendRgbSrcFunct;
				// Blend destination function (RGBA)
				Gl.Get(Gl.BLEND_DST, out blendRgbDstFunct);
				RenderException.DebugCheckErrors();
				blendAlphaDstFunct = blendRgbDstFunct;
			}

			// Store blending equation
			mRgbEquation = (BlendEquation)blendRgbEquation;
			mAlphaEquation = (BlendEquation)blendAlphaEquation;

			// Store blending function
			mRgbSrcFactor = (BlendFactor) blendRgbSrcFunct;
			mAlphaSrcFactor = (BlendFactor) blendAlphaSrcFunct;
			mRgbDstFactor = (BlendFactor) blendRgbDstFunct;
			mAlphaDstFactor = (BlendFactor) blendAlphaDstFunct;

			// Store blend color
			if (ctx.Caps.BlendColor.Supported) {
				float[] blendColor = new float[4];

				Gl.Get(Gl.BLEND_COLOR, blendColor);
				RenderException.DebugCheckErrors();

				mBlendColor = new ColorRGBAF(blendColor[0], blendColor[1], blendColor[2], blendColor[3]);
			}
		}

		#endregion
		
		#region Blend Render State Definition

		/// <summary>
		/// Equation that determine of source and destination fragments components are combined with each other.
		/// </summary>
		public enum BlendEquation
		{
			/// <summary>
			/// Add source and destination values.
			/// </summary>
			Add = Gl.FUNC_ADD,
			/// <summary>
			/// Compute the minimum between source and destination value.
			/// </summary>
			Min = Gl.MIN,
			/// <summary>
			/// Compute the maximum between source and destination value.
			/// </summary>
			Max = Gl.MAX,
			/// <summary>
			/// Subtract destination value from source value.
			/// </summary>
			Subtract = Gl.FUNC_SUBTRACT,
			/// <summary>
			/// Subtract source value from destination value.
			/// </summary>
			SubtractReverse = Gl.FUNC_REVERSE_SUBTRACT,
		}

		/// <summary>
		/// Weight factors that scale source and destination fragment color components.
		/// </summary>
		public enum BlendFactor
		{
			/// <summary>
			/// RGB blend factor (0,0,0); Alpha blend factor (0).
			/// </summary>
			Zero = Gl.ZERO,
			/// <summary>
			/// RGB blend factor (1,1,1); Alpha blend factor (1).
			/// </summary>
			One = Gl.ONE,
			/// <summary>
			/// RGB blend factor (Rs,Gs,Bs); Alpha blend factor (As).
			/// </summary>
			SrcColor = Gl.SRC_COLOR,
			/// <summary>
			/// RGB blend factor (1-Rs,1-Gs,1-Bs); Alpha blend factor (1-As).
			/// </summary>
			SrcColorComplement = Gl.ONE_MINUS_SRC_COLOR,
			/// <summary>
			/// RGB blend factor (Rd,Gd,Bd); Alpha blend factor (Ad).
			/// </summary>
			DstColor = Gl.DST_COLOR,
			/// <summary>
			/// RGB blend factor (1-Rd,1-Gd,1-Bd); Alpha blend factor (1-Ad).
			/// </summary>
			DstColorComplement = Gl.ONE_MINUS_DST_COLOR,
			/// <summary>
			/// RGB blend factor (As,As,As); Alpha blend factor (As).
			/// </summary>
			SrcAlpha = Gl.SRC_ALPHA,
			/// <summary>
			/// RGB blend factor (1-As,1-As,1-As); Alpha blend factor (1-As).
			/// </summary>
			SrcAlphaComplement = Gl.ONE_MINUS_SRC_ALPHA,
			/// <summary>
			/// RGB blend factor (f,f,f); Alpha blend factor (1). Where 'f'=min(As,1-Ad)
			/// </summary>
			SrcAlphaSaturate = Gl.SRC_ALPHA_SATURATE,
			/// <summary>
			/// RGB blend factor (Ad,Ad,Ad); Alpha blend factor (Ad).
			/// </summary>
			DstAlpha = Gl.DST_ALPHA,
			/// <summary>
			/// RGB blend factor (1-Ad,1-Ad,1-Ad); Alpha blend factor (1-Ad).
			/// </summary>
			DstAlphaComplement = Gl.ONE_MINUS_DST_ALPHA,
			/// <summary>
			/// RGB blend factor (Rc,Gc,Bc); Alpha blend factor (Ac).
			/// </summary>
			ConstColor = Gl.CONSTANT_COLOR,
			/// <summary>
			/// RGB blend factor (1-Rc,1-Gc,1-Bc); Alpha blend factor (1-Ac).
			/// </summary>
			ConstColorComplement = Gl.ONE_MINUS_CONSTANT_COLOR,
			/// <summary>
			/// RGB blend factor (Ac,Ac,Ac); Alpha blend factor (Ac).
			/// </summary>
			ConstAlpha = Gl.CONSTANT_ALPHA,
			/// <summary>
			/// RGB blend factor (1-Ac,1-Ac,1-Ac); Alpha blend factor (1-Ac).
			/// </summary>
			ConstAlphaComplement = Gl.ONE_MINUS_CONSTANT_ALPHA,
		}

		/// <summary>
		/// 
		/// </summary>
		public bool Enabled { get { return (mEnabled); } }
		
		/// <summary>
		/// Determine whether blending equation is separated for RGB and Alpha components.
		/// </summary>
		public bool EquationSeparated { get { return (mRgbEquation != mAlphaEquation); } }

		/// <summary>
		/// Blend equation for RGB components.
		/// </summary>
		public BlendEquation RgbEquation { get { return (mRgbEquation); } }

		/// <summary>
		/// Blend equation for alpha components.
		/// </summary>
		public BlendEquation AlphaEquation { get { return (mAlphaEquation); } }

		/// <summary>
		/// Determine whether blending function is separated for RGB and Alpha components.
		/// </summary>
		public bool FunctionSeparated { get { return ((mRgbSrcFactor != mAlphaSrcFactor) || (mRgbDstFactor != mAlphaDstFactor)); } }

		/// <summary>
		/// Constant blend color.
		/// </summary>
		public ColorRGBAF BlendColor { get { return (mBlendColor); } }

		/// <summary>
		/// RGB source blending factor.
		/// </summary>
		public BlendFactor RgbSrcFactor { get { return (mRgbSrcFactor); } }

		/// <summary>
		/// RGB destination blending factor.
		/// </summary>
		public BlendFactor RgbDstFactor { get { return (mRgbDstFactor); } }

		/// <summary>
		/// Alpha source blending factor.
		/// </summary>
		public BlendFactor AlphaSrcFactor { get { return (mAlphaSrcFactor); } }

		/// <summary>
		/// Alpha destination blending factor.
		/// </summary>
		public BlendFactor AlphaDstFactor { get { return (mAlphaDstFactor); } }

		/// <summary>
		/// Determine whether a blending function is supported.
		/// </summary>
		/// <param name="equation"></param>
		/// <returns></returns>
		private static bool IsSupportedEquation(BlendEquation equation)
		{
			switch (equation) {
				case BlendEquation.Min:
				case BlendEquation.Max:
					return (RenderContext.CurrentCaps.BlendMinMax.Supported);
				case BlendEquation.Subtract:
				case BlendEquation.SubtractReverse:
					return (RenderContext.CurrentCaps.BlendSubtract.Supported);
				default:
					return (true);
			}
		}

		/// <summary>
		/// Determine whether a blending function is supported.
		/// </summary>
		/// <param name="func"></param>
		/// <returns></returns>
		private static bool IsSupportedFunction(BlendFactor func)
		{
			switch (func) {
				case BlendFactor.ConstColor:
				case BlendFactor.ConstColorComplement:
				case BlendFactor.ConstAlpha:
				case BlendFactor.ConstAlphaComplement:
					return (RenderContext.CurrentCaps.BlendColor.Supported);
				default:
					return (true);
			}
		}

		/// <summary>
		/// Enabled flag.
		/// </summary>
		private readonly bool mEnabled;

		/// <summary>
		/// Blend equation for RGB components.
		/// </summary>
		private readonly BlendEquation mRgbEquation = BlendEquation.Add;

		/// <summary>
		/// Blend equation for alpha components.
		/// </summary>
		private readonly BlendEquation mAlphaEquation = BlendEquation.Add;
		
		/// <summary>
		/// RGB source blending factor.
		/// </summary>
		private readonly BlendFactor mRgbSrcFactor = BlendFactor.One;

		/// <summary>
		/// RGB destination blending factor.
		/// </summary>
		private readonly BlendFactor mRgbDstFactor = BlendFactor.Zero;

		/// <summary>
		/// Alpha source blending factor.
		/// </summary>
		private readonly BlendFactor mAlphaSrcFactor = BlendFactor.One;

		/// <summary>
		/// Alpha destination blending factor.
		/// </summary>
		private readonly BlendFactor mAlphaDstFactor = BlendFactor.Zero;

		/// <summary>
		/// Constant blending color.
		/// </summary>
		private readonly ColorRGBAF mBlendColor;
		
		#endregion

		#region Default State

		/// <summary>
		/// The system default state for BlendState.
		/// </summary>
		public static BlendState DefaultState { get { return (new BlendState()); } }

		#endregion

		#region RenderState Overrides

		/// <summary>
		/// The identifier for the blend state.
		/// </summary>
		public static readonly string StateId = "GL.Blend";

		/// <summary>
		/// The identifier of this RenderState.
		/// </summary>
		public override string Id { get { return (StateId); } }

		/// <summary>
		/// Flag indicating whether the state is context-bound.
		/// </summary>
		/// <remarks>
		/// It returns always true.
		/// </remarks>
		public override bool IsContextBound { get { return (true); } }

		/// <summary>
		/// Set ShaderProgram state.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> which has defined the shader program <paramref name="sProgram"/>.
		/// </param>
		/// <param name="sProgram">
		/// The <see cref="ShaderProgram"/> which has the state set.
		/// </param>
		public override void ApplyState(RenderContext ctx, ShaderProgram sProgram)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");

			if (Enabled) {
				// Enable blending
				Gl.Enable(Gl.BLEND);
				RenderException.DebugCheckErrors();

				// Set blending equation
				if (ctx.Caps.BlendMinMax.Supported == true) {
					if (EquationSeparated)
						Gl.BlendEquationSeparate((int) RgbEquation, (int) AlphaEquation);
					else
						Gl.BlendEquation((int) RgbEquation);
					RenderException.DebugCheckErrors();
				}

				// Set blending function
				if (FunctionSeparated)
					Gl.BlendFuncSeparate((int)mRgbSrcFactor, (int)mRgbDstFactor, (int)mAlphaSrcFactor, (int)mAlphaDstFactor);
				else
					Gl.BlendFunc((int)mRgbSrcFactor, (int)mRgbDstFactor);
				RenderException.DebugCheckErrors();
			} else {
				// Disable blending
				Gl.Disable(Gl.BLEND);
				RenderException.DebugCheckErrors();
			}
		}

		/// <summary>
		/// Indicates whether the current object is equal to another object of the same type.
		/// </summary>
		/// <param name="other">
		/// A <see cref="RenderState"/> to compare to this RenderState.
		/// </param>
		/// <returns>
		/// It returns true if the current object is equal to <paramref name="other"/>.
		/// </returns>
		/// <remarks>
		/// <para>
		/// This method test only whether <paramref name="other"/> type equals to this type.
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentNullException">
		/// This exception is thrown if the parameter <paramref name="other"/> is null.
		/// </exception>
		public override bool Equals(IRenderState other)
		{
			if (base.Equals(other) == false)
				return (false);
			Debug.Assert(other is BlendState);

			BlendState otherState = (BlendState) other;

			if (otherState.mEnabled != mEnabled)
				return (false);
			if ((otherState.RgbEquation != RgbEquation) || (otherState.AlphaEquation != AlphaEquation))
				return (false);
			if ((otherState.RgbSrcFactor != RgbSrcFactor) || (otherState.RgbDstFactor != RgbDstFactor))
				return (false);
			if ((otherState.AlphaSrcFactor != AlphaSrcFactor) || (otherState.AlphaDstFactor != AlphaDstFactor))
				return (false);

			return (true);
		}

		/// <summary>
		/// Performs a deep copy of this <see cref="IRenderState"/>.
		/// </summary>
		/// <returns>
		/// It returns the equivalent of this <see cref="IRenderState"/>, but all objects referenced
		/// are not referred by both instances.
		/// </returns>
		public override IRenderState Copy()
		{
			return (new BlendState(RgbEquation, AlphaEquation, RgbSrcFactor, AlphaSrcFactor, RgbDstFactor, AlphaDstFactor, BlendColor));
		}
		
		#endregion
	}
}
